﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using stunnware.CRM.Fetch;
using System.Collections.Specialized;


namespace LinqtoCRM
{
    internal class QueryFormatter : ExpressionVisitor
    {
        FetchStatement stmt;
        Dictionary<string, FetchLinkEntity> attributeMap = new Dictionary<string, FetchLinkEntity>();

        internal Delegate Projector { get; private set; }

        private CrmQueryProvider _provider;
        private WhereBuilder _whereBuilder;

        internal QueryFormatter(CrmQueryProvider provider)
        {
            _provider = provider;
        }

        internal FetchStatement Format(Expression expression)
        {
            this.stmt = new FetchStatement();
            this.Visit(expression);
            Projector = new ProjectionBuilder(_provider, stmt, expression).Projector;
            return this.stmt;
        }

        protected override Expression VisitMethodCall(MethodCallExpression m)
        {
            if (m.Method.DeclaringType == typeof(Queryable) || m.Method.DeclaringType == typeof(Enumerable))
            {
                switch (m.Method.Name)
                {
                    case "Where":
                        {
                            base.VisitMethodCall(m); /* stmt.Entity etc gets setup here */
                            // Build conditions from the predicate
                            _whereBuilder = new WhereBuilder(this.stmt,m.Arguments[1]);
                            _whereBuilder.Process();
                            break;
                        }
                    case "Select":
                    case "SelectMany":
                        {
                            /* Need these so that we don't fall back on the exception */
                            base.VisitMethodCall(m);
                            break;
                        }
                    case "Join":
                    case "GroupJoin":
                        {
                            this.VisitJoin(m);
                            break;
                        }
                    case "OrderByDescending":
                    case "OrderBy":
                    case "ThenBy":
                    case "ThenByDescending":
                        {
                            this.VisitOrderBy(m);
                            break;
                        }
                    case "Take":
                        {
                            this.VisitTake(m);
                            break;
                        }
                    case "Skip":
                        {
                            this.VisitSkip(m);
                            break;
                        }

                    case "Count":
                        {
                            this.VisitCount(m);
                            break;
                        }

                    case "Distinct":
                        {
                            this.Visit(m.Arguments[0]);
                            this.VisitDistinct(m);
                            break;
                        }
                    case "DefaultIfEmpty":
                        break;
                    //case "Average":
                    //    {
                    //        this.VisitAverage(m);
                    //        break;
                    //    }

                    default:
                        throw new NotSupportedException(string.Format("The method '{0}' is not supported", m.Method.Name));
                }
            }
            return m;
        }

        private void VisitDistinct(MethodCallExpression m)
        {
            stmt.Distinct = true;
        }

        private void VisitCount(MethodCallExpression m)
        {
            foreach (Expression exp in m.Arguments)
            {
                base.Visit(exp);
            }
            this.stmt.Aggregate = true;
            this.stmt.Entity.Columns.Columns = new StringCollection();
            this.stmt.Entity.Columns.Aggregate = FetchAggregate.Count;
            this.stmt.Entity.Columns.Alias = "count";
            this.stmt.Entity.Columns.AllColumns = false;
            this.stmt.Entity.Columns.Columns.Add(this.stmt.Entity.EntityName + "id");
        }

        //private void VisitAverage(MethodCallExpression m)
        //{
        //    foreach (Expression exp in m.Arguments)
        //    {
        //        base.Visit(exp);
        //    }
        //    this.stmt.Aggregate = true;
        //    //this.stmt.Entity.Columns.Columns = new StringCollection();
        //    this.stmt.Entity.Columns.Aggregate = FetchAggregate.Average;
        //    //this.stmt.Entity.Columns.Alias = "count";
        //    this.stmt.Entity.Columns.AllColumns = false;
        //    //this.stmt.Entity.Columns.Columns.Add(this.stmt.Entity.EntityName + "id");
        //}

        /* praty: I don't see a need to restrict types here */
        //protected override Expression VisitUnary(UnaryExpression u)
        //{
        //    switch (u.NodeType)
        //    {
        //        case ExpressionType.Not:
        //            this.Visit(u.Operand);
        //            break;
        //        case ExpressionType.Quote:
        //            this.Visit(u.Operand);
        //            break;
        //        default:
        //            throw new NotSupportedException(string.Format("The unary operator '{0}' is not supported", u.NodeType));
        //    }
        //    return u;
        //}

        protected override Expression VisitConstant(ConstantExpression c)
        {
            object value = c.Value;
            //TODO: There should be a better way than relying on the first constant expression
            if (stmt.Entity == null && value != null &&
                Type.GetTypeCode(value.GetType()) == TypeCode.Object) {
                stmt.Entity = new FetchEntity(c.Value.GetType().GetGenericArguments()[0].Name);
                stmt.Entity.Columns = new FetchColumnSet();
                stmt.Entity.Columns.Columns = new System.Collections.Specialized.StringCollection();
            }
            return c;
        }

        private void VisitSkip(MethodCallExpression skip)
        {
            this.Visit(skip.Arguments[0]);
            stmt.PageNumber = (int)Expression.Lambda<Func<int>>(skip.Arguments[1]).Compile().DynamicInvoke();
        }

        private void VisitTake(MethodCallExpression take)
        {
            this.Visit(take.Arguments[0]);
            int pageCount = (int)Expression.Lambda<Func<int>>(take.Arguments[1]).Compile().DynamicInvoke();
            if (pageCount == 0)
                throw new ArgumentException("The method Take() cannot take 0 items");
            stmt.PageCount = stmt.PageCount == 0 ? pageCount : Math.Min(stmt.PageCount, pageCount);
            stmt.PageNumber = stmt.PageNumber / stmt.PageCount + 1;
        }

        private void VisitOrderBy(MethodCallExpression m)
        {
            // visit the source
            this.Visit(m.Arguments[0]);
            // BUG: Check that order attribute is on primary (non-linked/joined) entity
            //FIX: check if the attributemap contains the member
            MemberExpression e = ((m.Arguments[1] as UnaryExpression).Operand as LambdaExpression).Body as MemberExpression;
            FetchOrder order = new FetchOrder(e.Member.Name);

            if (m.Method.Name.Contains("Descending"))
            {
                order.Descending = true;
            }
            ParameterExpression pex = e.Expression as ParameterExpression;
            if (pex != null)
            {
                stmt.Entity.Orders.Add(order);
            }
            else
            {
                MemberExpression mex = e.Expression as MemberExpression;
                if (mex != null)
                {
                    string key = mex.Type.Name;
                    if (attributeMap.ContainsKey(key))
                    {
                        attributeMap[key].Orders.Add(order);
                    }
                    else
                    {
                        stmt.Entity.Orders.Add(order);
                    }
                }
            }
        }

        private void VisitJoin(MethodCallExpression join)
        {
            // this will set up the correct Fetchentity
            this.Visit(join.Arguments[0]);

            // TODO: Handle non-dotted values
            MemberExpression outerMemberExp = (((join.Arguments[2] as UnaryExpression).Operand as LambdaExpression).Body as MemberExpression).Expression as MemberExpression;
            MemberExpression innerMemberExp = (((join.Arguments[3] as UnaryExpression).Operand as LambdaExpression).Body as MemberExpression).Expression as MemberExpression;
            string outerTypeName = outerMemberExp.Expression.Type.Name;
            string innerTypeName = innerMemberExp.Expression.Type.Name;

            FetchLinkEntity link = new FetchLinkEntity(innerTypeName);
            link.From = innerMemberExp.Member.Name;
            link.To = outerMemberExp.Member.Name;
            link.Columns = new FetchColumnSet();
            link.Columns.Columns = new System.Collections.Specialized.StringCollection();
            link.LinkType = (join.Method.Name == "GroupJoin") ? FetchLinkType.Outer : FetchLinkType.Default;

            if (outerTypeName == stmt.Entity.EntityName)
            {
                // add to the top entity
                stmt.Entity.LinkEntities.Add(link);
            }
            else
            {
                // TODO: this seems imperfect
                // seach for one of same type, and add link to that

                FetchLinkEntity l = attributeMap[outerTypeName];
                if (l == null)
                { throw new ArgumentException("Problem adding linked entity"); }
                l.LinkEntities.Add(link);
            }
            //}

            // SAN trying to add link to attributemap
            // TODO: MFR, consider circurlar joins?
            attributeMap.Add(innerTypeName, link);
        }

        internal static Expression StripQuotes(Expression e)
        {
            while (e.NodeType == ExpressionType.Quote)
            {
                e = ((UnaryExpression)e).Operand;
            }
            return e;
        }
    }
}
